]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
workspace: Use all available space for windows in window selector
authorJasper St. Pierre <jstpierre@mecheye.net>
Wed, 8 Aug 2012 17:27:46 +0000 (14:27 -0300)
committerJasper St. Pierre <jstpierre@mecheye.net>
Tue, 23 Oct 2012 19:12:45 +0000 (15:12 -0400)
Change the layout strategy to be more like the mockups. With less than
two rows of windows, we try to fit every window in a non-aligned situation;
with more than three rows of windows, we try to fit every window in an
aligned situation.

Based heavily on a patch from Pierre-Eric Pelloux-Prayer <pelloux@gmail.com>

https://bugzilla.gnome.org/show_bug.cgi?id=582650

data/theme/gnome-shell.css
js/ui/workspace.js

index d21c7e9377b91a14b4d14bb64b6203aba8530ce7..dd492bf01d389eb00a7eddddec38468bc76a1b9e 100644 (file)
@@ -640,6 +640,11 @@ StScrollBar StButton#vhandle:active {
     -st-background-image-shadow: 2px 2px 6px rgba(0,0,0,0.5);
 }
 
+.window-picker {
+    -horizontal-spacing: 40px;
+    -vertical-spacing: 40px;
+}
+
 /* Dash */
 
 #dash {
index 8edab8b62d2a1ebde53477e143d52e5f7850aef8..2be99fc9c0b88f5855a5cf21c48762441a91cc85 100644 (file)
@@ -33,21 +33,13 @@ const DRAGGING_WINDOW_OPACITY = 100;
 const BUTTON_LAYOUT_SCHEMA = 'org.gnome.shell.overrides';
 const BUTTON_LAYOUT_KEY = 'button-layout';
 
-// Define a layout scheme for small window counts. For larger
-// counts we fall back to an algorithm. We need more schemes here
-// unless we have a really good algorithm.
-
-// Each triplet is [xCenter, yCenter, scale] where the scale
-// is relative to the width of the workspace.
-const POSITIONS = {
-        1: [[0.5, 0.5, 0.95]],
-        2: [[0.25, 0.5, 0.48], [0.75, 0.5, 0.48]],
-        3: [[0.25, 0.25, 0.48],  [0.75, 0.25, 0.48],  [0.5, 0.75, 0.48]],
-        4: [[0.25, 0.25, 0.47],   [0.75, 0.25, 0.47], [0.75, 0.75, 0.47], [0.25, 0.75, 0.47]],
-        5: [[0.165, 0.25, 0.32], [0.495, 0.25, 0.32], [0.825, 0.25, 0.32], [0.25, 0.75, 0.32], [0.75, 0.75, 0.32]]
-};
-// Used in _orderWindowsPermutations, 5! = 120 which is probably the highest we can go
-const POSITIONING_PERMUTATIONS_MAX = 5;
+// When calculating a layout, we calculate the scale of windows and the percent
+// of the available area the new layout uses. If the values for the new layout,
+// when weighted with the values as below, are worse than the previous layout's,
+// we stop looking for a new layout and use the previous layout.
+// Otherwise, we keep looking for a new layout.
+const LAYOUT_SCALE_WEIGHT = 1;
+const LAYOUT_SPACE_WEIGHT = 0.1;
 
 function _interpolate(start, end, step) {
     return start + (end - start) * step;
@@ -504,10 +496,6 @@ const WindowOverlay = new Lang.Class({
                            transition: 'easeOutQuad' });
     },
 
-    chromeWidth: function () {
-        return this.closeButton.width - this.closeButton._overlap;
-    },
-
     chromeHeights: function () {
         return [this.closeButton.height - this.closeButton._overlap,
                 this.title.height + this.title._spacing];
@@ -668,6 +656,280 @@ const WindowPositionFlags = {
     ANIMATE: 1 << 1
 };
 
+const LayoutStrategy = new Lang.Class({
+    Name: 'LayoutStrategy',
+    Abstract: true,
+
+    _init: function(monitor, rowSpacing, columnSpacing, bottomPadding) {
+        this._monitor = monitor;
+        this._rowSpacing = rowSpacing;
+        this._columnSpacing = columnSpacing;
+        this._bottomPadding = bottomPadding;
+    },
+
+    _newRow: function() {
+        // Row properties:
+        //
+        // * x, y are the position of row, relative to area
+        //
+        // * width, height are the scaled versions of fullWidth, fullHeight
+        //
+        // * width also has the spacing in between windows. It's not in
+        //   fullWidth, as the spacing is constant, whereas fullWidth is
+        //   meant to be scaled
+        //
+        // * neither height/fullHeight have any sort of spacing or padding
+        //
+        // * if cellWidth is present, all windows in the row will occupy
+        //   the space of cellWidth, centered.
+        return { x: 0, y: 0,
+                 width: 0, height: 0,
+                 fullWidth: 0, fullHeight: 0,
+                 cellWidth: 0,
+                 windows: [] };
+    },
+
+    // Compute the size and fancy scale for @window using the
+    // base scale, @scale.
+    //
+    // Returns a list structure: [ scaledWidth, scaledHeight, fancyScale ]
+    // where scaledWidth and scaledHeight are the window's
+    // width and height, scaled by fancyScale for convenience.
+    _computeWindowSizeAndScale: function(window, scale) {
+        let width = window.actor.width;
+        let height = window.actor.height;
+        let ratio;
+
+        if (width > height)
+            ratio = width / this._monitor.width;
+        else
+            ratio = height / this._monitor.height;
+
+        let fancyScale = (2 / (1 + ratio)) * scale;
+        return [width * fancyScale, height * fancyScale, fancyScale];
+    },
+
+    // Compute the size of each row, by assigning to the properties
+    // row.width, row.height, row.fullWidth, row.fullHeight, and
+    // (optionally) row.cellWidth, for each row in @layout.rows.
+    // This method is intended to be called by subclasses.
+    _computeRowSizes: function(layout) {
+        throw new Error('_computeRowSizes not implemented');
+    },
+
+    // Compute strategy-specific window slots for each window in
+    // @windows, given the @layout. The strategy may also use @layout
+    // as strategy-specific storage.
+    //
+    // This must calculate:
+    //  * maxColumns - The maximum number of columns used by the layout.
+    //  * gridWidth - The total width used by the grid, unscaled, unspaced.
+    //  * gridHeight - The totial height used by the grid, unscaled, unspaced.
+    //  * rows - A list of rows, which should be instantiated by _newRow.
+    computeLayout: function(windows, layout) {
+        throw new Error('computeLayout not implemented');
+    },
+
+    // Given @layout, compute the overall scale and space of the layout.
+    // The scale is the individual, non-fancy scale of each window, and
+    // the space is the percentage of the available area eventually
+    // used by the layout.
+
+    // This method does not return anything, but instead installs
+    // the properties "scale" and "space" on @layout directly.
+    //
+    // Make sure to call this methods before calling computeWindowSlots(),
+    // as it depends on the scale property installed in @layout here.
+    computeScaleAndSpace: function(layout) {
+        let area = layout.area;
+
+        let hspacing = (layout.maxColumns - 1) * this._columnSpacing;
+        let vspacing = (layout.numRows - 1) * this._rowSpacing + this._bottomPadding;
+
+        let spacedWidth = area.width - hspacing;
+        let spacedHeight = area.height - vspacing;
+
+        let horizontalScale = spacedWidth / layout.gridWidth;
+        let verticalScale = spacedHeight / layout.gridHeight;
+
+        // Thumbnails should be less than 70% of the original size
+        let scale = Math.min(horizontalScale, verticalScale, WINDOW_CLONE_MAXIMUM_SCALE);
+
+        let scaledLayoutWidth = layout.gridWidth * scale + hspacing;
+        let scaledLayoutHeight = layout.gridHeight * scale + vspacing;
+        let space = (scaledLayoutWidth * scaledLayoutHeight) / (area.width * area.height);
+
+        layout.scale = scale;
+        layout.space = space;
+    },
+
+    computeWindowSlots: function(layout, area) {
+        this._computeRowSizes(layout);
+
+        let { rows: rows, scale: scale, state: state } = layout;
+
+        let slots = [];
+
+        let y = 0;
+        for (let i = 0; i < rows.length; i++) {
+            let row = rows[i];
+            row.x = area.x + (area.width - row.width) / 2;
+            row.y = area.y + y;
+            y += row.height + this._rowSpacing;
+        }
+
+        let height = y - this._rowSpacing + this._bottomPadding;
+        let baseY = (area.height - height) / 2;
+
+        for (let i = 0; i < rows.length; i++) {
+            let row = rows[i];
+            row.y += baseY;
+            let baseX = row.x;
+            for (let j = 0; j < row.windows.length; j++) {
+                let window = row.windows[j];
+
+                let [width, height, s] = this._computeWindowSizeAndScale(window, scale);
+                let y = row.y + row.height - height;
+
+                let x = baseX;
+                if (row.cellWidth) {
+                    x += (row.cellWidth - width) / 2;
+                    width = row.cellWidth;
+                }
+
+                slots.push([x, y, s]);
+                baseX += width + this._columnSpacing;
+            }
+        }
+        return slots;
+    }
+});
+
+const UnalignedLayoutStrategy = new Lang.Class({
+    Name: 'UnalignedLayoutStrategy',
+    Extends: LayoutStrategy,
+
+    _computeRowSizes: function(layout) {
+        let { rows: rows, scale: scale } = layout;
+        for (let i = 0; i < rows.length; i++) {
+            let row = rows[i];
+            row.width = row.fullWidth * scale + (row.windows.length - 1) * this._columnSpacing;
+            row.height = row.fullHeight * scale;
+        }
+    },
+
+    _keepSameRow: function(row, window, width, idealRowWidth) {
+        if (row.fullWidth + width <= idealRowWidth)
+            return true;
+
+        let oldRatio = row.fullWidth / idealRowWidth;
+        let newRatio = (row.fullWidth + width) / idealRowWidth;
+
+        if (Math.abs(1 - newRatio) < Math.abs(1 - oldRatio))
+            return true;
+
+        return false;
+    },
+
+    computeLayout: function(windows, layout) {
+        let numRows = layout.numRows;
+
+        let rows = [];
+        let totalWidth = 0;
+        for (let i = 0; i < windows.length; i++) {
+            totalWidth += windows[i].actor.width;
+        }
+
+        let idealRowWidth = totalWidth / numRows;
+        let windowIdx = 0;
+        for (let i = 0; i < numRows; i++) {
+            let col = 0;
+            let row = this._newRow();
+            rows.push(row);
+
+            for (; windowIdx < windows.length; windowIdx++) {
+                let window = windows[windowIdx];
+                let [width, height] = this._computeWindowSizeAndScale(window, 1);
+                row.fullHeight = Math.max(row.fullHeight, height);
+
+                // either new width is < idealWidth or new width is nearer from idealWidth then oldWidth
+                if (this._keepSameRow(row, window, width, idealRowWidth) || (i == numRows - 1)) {
+                    row.windows.push(window);
+                    row.fullWidth += width;
+                } else {
+                    break;
+                }
+            }
+        }
+
+        let gridHeight = 0;
+        let maxRow;
+        for (let i = 0; i < numRows; i++) {
+            let row = rows[i];
+            if (!maxRow || row.fullWidth > maxRow.fullWidth)
+                maxRow = row;
+            gridHeight += row.fullHeight;
+        }
+
+        layout.rows = rows;
+        layout.maxColumns = maxRow.windows.length;
+        layout.gridWidth = maxRow.fullWidth;
+        layout.gridHeight = gridHeight;
+    }
+});
+
+const GridLayoutStrategy = new Lang.Class({
+    Name: 'GridLayoutStrategy',
+    Extends: LayoutStrategy,
+
+    _computeRowSizes: function(layout) {
+        let { rows: rows, scale: scale } = layout;
+
+        let gridWidth = layout.numColumns * layout.maxWindowWidth;
+        let hspacing = (layout.numColumns - 1) * this._columnSpacing;
+        for (let i = 0; i < rows.length; i++) {
+            let row = rows[i];
+            row.fullWidth = layout.gridWidth;
+            row.fullHeight = layout.maxWindowHeight;
+
+            row.width = row.fullWidth * scale + hspacing;
+            row.height = row.fullHeight * scale;
+            row.cellWidth = layout.maxWindowWidth * scale;
+        }
+    },
+
+    computeLayout: function(windows, layout) {
+        let { numRows: numRows, numColumns: numColumns } = layout;
+        let rows = [];
+        let windowIdx = 0;
+
+        let maxWindowWidth = 0;
+        let maxWindowHeight = 0;
+        for (let i = 0; i < numRows; i++) {
+            let row = this._newRow();
+            rows.push(row);
+            for (; windowIdx < windows.length; windowIdx++) {
+                if (row.windows.length >= numColumns)
+                    break;
+
+                let window = windows[windowIdx];
+                row.windows.push(window);
+
+                let [width, height] = this._computeWindowSizeAndScale(window, 1);
+                maxWindowWidth = Math.max(maxWindowWidth, width);
+                maxWindowHeight = Math.max(maxWindowHeight, height);
+            }
+        }
+
+        layout.rows = rows;
+        layout.maxColumns = numColumns;
+        layout.gridWidth = numColumns * maxWindowWidth;
+        layout.gridHeight = numRows * maxWindowHeight;
+        layout.maxWindowWidth = maxWindowWidth;
+        layout.maxWindowHeight = maxWindowHeight;
+    }
+});
+
 /**
  * @metaWorkspace: a #Meta.Workspace, or null
  */
@@ -689,7 +951,7 @@ const Workspace = new Lang.Class({
         // Without this the drop area will be overlapped.
         this._windowOverlaysGroup.set_size(0, 0);
 
-        this.actor = new Clutter.Group();
+        this.actor = new St.Widget({ style_class: 'window-picker' });
         this.actor.set_size(0, 0);
 
         this._dropRect = new Clutter.Rectangle({ opacity: 0 });
@@ -763,225 +1025,6 @@ const Workspace = new Lang.Class({
         return this._windows.length == 0;
     },
 
-    // Only use this for n <= 20 say
-    _factorial: function(n) {
-        let result = 1;
-        for (let i = 2; i <= n; i++)
-            result *= i;
-        return result;
-    },
-
-    /**
-     * _permutation:
-     * @permutationIndex: An integer from [0, list.length!)
-     * @list: (inout): Array of objects to permute; will be modified in place
-     *
-     * Given an integer between 0 and length of array, re-order the array in-place
-     * into a permutation denoted by the index.
-     */
-    _permutation: function(permutationIndex, list) {
-        for (let j = 2; j <= list.length; j++) {
-            let firstIndex = (permutationIndex % j);
-            let secondIndex = j - 1;
-            // Swap
-            let tmp = list[firstIndex];
-            list[firstIndex] = list[secondIndex];
-            list[secondIndex] = tmp;
-            permutationIndex = Math.floor(permutationIndex / j);
-        }
-    },
-
-    /**
-     * _forEachPermutations:
-     * @list: Array
-     * @func: Function which takes a single array argument
-     *
-     * Call @func with each permutation of @list as an argument.
-     */
-    _forEachPermutations: function(list, func) {
-        let nCombinations = this._factorial(list.length);
-        for (let i = 0; i < nCombinations; i++) {
-            let listCopy = list.concat();
-            this._permutation(i, listCopy);
-            func(listCopy);
-        }
-    },
-
-    /**
-     * _computeWindowMotion:
-     * @actor: A #WindowClone's #ClutterActor
-     * @slot: An element of #POSITIONS
-     * @slotGeometry: Layout of @slot
-     *
-     * Returns a number corresponding to how much perceived motion
-     * would be involved in moving the window to the given slot.
-     * Currently this is the square of the distance between the
-     * centers.
-     */
-    _computeWindowMotion: function (actor, slot) {
-        let [xCenter, yCenter, fraction] = slot;
-        let xDelta, yDelta, distanceSquared;
-        let actorWidth, actorHeight;
-
-        let x = actor.x;
-        let y = actor.y;
-        let scale = actor.scale_x;
-
-        if (actor._delegate.inDrag) {
-            x = actor._delegate.dragOrigX;
-            y = actor._delegate.dragOrigY;
-            scale = actor._delegate.dragOrigScale;
-        }
-
-        actorWidth = actor.width * scale;
-        actorHeight = actor.height * scale;
-        xDelta = x + actorWidth / 2.0 - xCenter * this._width - this._x;
-        yDelta = y + actorHeight / 2.0 - yCenter * this._height - this._y;
-        distanceSquared = xDelta * xDelta + yDelta * yDelta;
-
-        return distanceSquared;
-    },
-
-    /**
-     * _orderWindowsPermutations:
-     *
-     * Iterate over all permutations of the windows, and determine the
-     * permutation which has the least total motion.
-     */
-    _orderWindowsPermutations: function (clones, slots) {
-        let minimumMotionPermutation = null;
-        let minimumMotion = -1;
-        let permIndex = 0;
-        this._forEachPermutations(clones, Lang.bind(this, function (permutation) {
-            let motion = 0;
-            for (let i = 0; i < permutation.length; i++) {
-                let cloneActor = permutation[i].actor;
-                let slot = slots[i];
-
-                let delta = this._computeWindowMotion(cloneActor, slot);
-
-                motion += delta;
-
-                // Bail out early if we're already larger than the
-                // previous best
-                if (minimumMotionPermutation != null &&
-                    motion > minimumMotion)
-                    continue;
-            }
-
-            if (minimumMotionPermutation == null || motion < minimumMotion) {
-                minimumMotionPermutation = permutation;
-                minimumMotion = motion;
-            }
-            permIndex++;
-        }));
-        return minimumMotionPermutation;
-    },
-
-    /**
-     * _orderWindowsGreedy:
-     *
-     * Iterate over available slots in order, placing into each one the window
-     * we find with the smallest motion to that slot.
-     */
-    _orderWindowsGreedy: function(clones, slots) {
-        let result = [];
-        let slotIndex = 0;
-        // Copy since we mutate below
-        let clonesCopy = clones.concat();
-        for (let i = 0; i < slots.length; i++) {
-            let slot = slots[i];
-            let minimumMotionIndex = -1;
-            let minimumMotion = -1;
-            for (let j = 0; j < clonesCopy.length; j++) {
-                let cloneActor = clonesCopy[j].actor;
-                let delta = this._computeWindowMotion(cloneActor, slot);
-                if (minimumMotionIndex == -1 || delta < minimumMotion) {
-                    minimumMotionIndex = j;
-                    minimumMotion = delta;
-                }
-            }
-            result.push(clonesCopy[minimumMotionIndex]);
-            clonesCopy.splice(minimumMotionIndex, 1);
-        }
-        return result;
-    },
-
-    /**
-     * _orderWindowsByMotionAndStartup:
-     * @windows: Array of #MetaWindow
-     * @slots: Array of slots
-     *
-     * Returns a copy of @windows, ordered in such a way that they require least motion
-     * to move to the final screen coordinates of @slots.  Ties are broken in a stable
-     * fashion by the order in which the windows were created.
-     */
-    _orderWindowsByMotionAndStartup: function(clones, slots) {
-        clones.sort(function(w1, w2) {
-            return w2.metaWindow.get_stable_sequence() - w1.metaWindow.get_stable_sequence();
-        });
-        if (clones.length <= POSITIONING_PERMUTATIONS_MAX)
-            return this._orderWindowsPermutations(clones, slots);
-        else
-            return this._orderWindowsGreedy(clones, slots);
-    },
-
-    /**
-     * _getSlotGeometry:
-     * @slot: A layout slot
-     *
-     * Returns: the screen-relative [x, y, width, height]
-     * of a given window layout slot.
-     */
-    _getSlotGeometry: function(slot) {
-        let [xCenter, yCenter, fraction] = slot;
-
-        let width = this._width * fraction;
-        let height = this._height * fraction;
-
-        let x = this._x + xCenter * this._width - width / 2 ;
-        let y = this._y + yCenter * this._height - height / 2;
-
-        return [x, y, width, height];
-    },
-
-    /**
-     * _computeWindowLayout:
-     * @metaWindow: A #MetaWindow
-     * @slot: A layout slot
-     *
-     * Given a window and slot to fit it in, compute its
-     * screen-relative [x, y, scale] where scale applies
-     * to both X and Y directions.
-     */
-    _computeWindowLayout: function(metaWindow, slot) {
-        let [x, y, width, height] = this._getSlotGeometry(slot);
-
-        let rect = metaWindow.get_outer_rect();
-        let buttonOuterHeight, captionHeight;
-        let buttonOuterWidth = 0;
-
-        if (this._windowOverlays[0]) {
-            [buttonOuterHeight, captionHeight] = this._windowOverlays[0].chromeHeights();
-            buttonOuterWidth = this._windowOverlays[0].chromeWidth();
-        } else
-            [buttonOuterHeight, captionHeight] = [0, 0];
-
-        let scale = Math.min((width - buttonOuterWidth) / rect.width,
-                             (height - buttonOuterHeight - captionHeight) / rect.height,
-                             WINDOW_CLONE_MAXIMUM_SCALE);
-
-        x = Math.floor(x + (width - scale * rect.width) / 2);
-
-        // We want to center the window in case we have just one
-        if (metaWindow.get_workspace().n_windows == 1)
-            y = Math.floor(y + (height - scale * rect.height) / 2);
-        else
-            y = Math.floor(y + height - rect.height * scale - captionHeight);
-
-        return [x, y, scale];
-    },
-
     setReservedSlot: function(clone) {
         if (this._reservedSlot == clone)
             return;
@@ -1020,6 +1063,11 @@ const Workspace = new Lang.Class({
         }
 
         let clones = this._windows.slice();
+
+        clones.sort(function(a, b) {
+            return a.metaWindow.get_stable_sequence() - b.metaWindow.get_stable_sequence();
+        });
+
         if (this._reservedSlot)
             clones.push(this._reservedSlot);
 
@@ -1027,8 +1075,7 @@ const Workspace = new Lang.Class({
         let animate = flags & WindowPositionFlags.ANIMATE;
 
         // Start the animations
-        let slots = this._computeAllWindowSlots(clones.length);
-        clones = this._orderWindowsByMotionAndStartup(clones, slots);
+        let slots = this._computeAllWindowSlots(clones);
 
         let currentWorkspace = global.screen.get_active_workspace();
         let isOnCurrentWorkspace = this.metaWorkspace == null || this.metaWorkspace == currentWorkspace;
@@ -1045,10 +1092,11 @@ const Workspace = new Lang.Class({
             if (clone.inDrag)
                 continue;
 
-            let [x, y, scale] = this._computeWindowLayout(metaWindow, slot);
+            let [x, y, scale] = slot;
 
             if (overlay && initialPositioning)
                 overlay.hide();
+
             if (animate && isOnCurrentWorkspace) {
                 if (!metaWindow.showing_on_its_workspace()) {
                     /* Hidden windows should fade in and grow
@@ -1072,6 +1120,8 @@ const Workspace = new Lang.Class({
 
                 this._animateClone(clone, overlay, x, y, scale, initialPositioning);
             } else {
+                // cancel any active tweens (otherwise they might override our changes)
+                Tweener.removeTweens(clone.actor);
                 clone.actor.set_position(x, y);
                 clone.actor.set_scale(scale, scale);
                 this._updateWindowOverlayPositions(clone, overlay, x, y, scale, false);
@@ -1113,11 +1163,11 @@ const Workspace = new Lang.Class({
     },
 
     _updateWindowOverlayPositions: function(clone, overlay, x, y, scale, animate) {
+        if (!overlay)
+            return;
+
         let [cloneWidth, cloneHeight] = clone.actor.get_size();
-        cloneWidth = scale * cloneWidth;
-        cloneHeight = scale * cloneHeight;
-        if (overlay)
-            overlay.updatePositions(x, y, cloneWidth, cloneHeight, animate);
+        overlay.updatePositions(x, y, cloneWidth * scale, cloneHeight * scale, animate);
     },
 
     _showWindowOverlay: function(clone, overlay, fade) {
@@ -1464,29 +1514,90 @@ const Workspace = new Lang.Class({
         }
     },
 
-    _computeWindowSlot : function(windowIndex, numberOfWindows) {
-        if (numberOfWindows in POSITIONS)
-            return POSITIONS[numberOfWindows][windowIndex];
+    _isBetterLayout: function(oldLayout, newLayout) {
+        if (oldLayout.scale === undefined)
+            return true;
 
-        // If we don't have a predefined scheme for this window count,
-        // arrange the windows in a grid pattern.
-        let gridWidth = Math.ceil(Math.sqrt(numberOfWindows));
-        let gridHeight = Math.ceil(numberOfWindows / gridWidth);
+        let spacePower = (newLayout.space - oldLayout.space) * LAYOUT_SPACE_WEIGHT;
+        let scalePower = (newLayout.scale - oldLayout.scale) * LAYOUT_SCALE_WEIGHT;
 
-        let fraction = 0.95 * (1. / gridWidth);
+        if (newLayout.scale > oldLayout.scale && newLayout.space > oldLayout.space) {
+            // Win win -- better scale and better space
+            return true;
+        } else if (newLayout.scale > oldLayout.scale && newLayout.space <= oldLayout.space) {
+            // Keep new layout only if scale gain outweights aspect space loss
+            return scalePower > spacePower;
+        } else if (newLayout.scale <= oldLayout.scale && newLayout.space > oldLayout.space) {
+            // Keep new layout only if aspect space gain outweights scale loss
+            return spacePower > scalePower;
+        } else {
+            // Lose -- worse scale and space
+            return false;
+        }
+    },
+
+    _computeLayout: function(windows, area, rowSpacing, columnSpacing, bottomPadding) {
+        // We look for the largest scale that allows us to fit the
+        // largest row/tallest column on the workspace.
+
+        let lastLayout = {};
+
+        for (let numRows = 1; ; numRows++) {
+            let numColumns = Math.ceil(windows.length / numRows);
 
-        let xCenter = (.5 / gridWidth) + ((windowIndex) % gridWidth) / gridWidth;
-        let yCenter = (.5 / gridHeight) + Math.floor((windowIndex / gridWidth)) / gridHeight;
+            // If adding a new row does not change column count just stop
+            // (for instance: 9 windows, with 3 rows -> 3 columns, 4 rows ->
+            // 3 columns as well => just use 3 rows then)
+            if (numColumns == lastLayout.numColumns)
+                break;
+
+            let strategyClass = numRows > 2 ? GridLayoutStrategy : UnalignedLayoutStrategy;
+            let strategy = new strategyClass(this._monitor, rowSpacing, columnSpacing, bottomPadding);
+
+            let layout = { area: area, strategy: strategy, numRows: numRows, numColumns: numColumns };
+            strategy.computeLayout(windows, layout);
+            strategy.computeScaleAndSpace(layout);
+
+            if (!this._isBetterLayout(lastLayout, layout))
+                break;
+
+            lastLayout = layout;
+        }
 
-        return [xCenter, yCenter, fraction];
+        return lastLayout;
     },
 
-    _computeAllWindowSlots: function(totalWindows) {
-        let slots = [];
-        for (let i = 0; i < totalWindows; i++) {
-            slots.push(this._computeWindowSlot(i, totalWindows));
+    _computeAllWindowSlots: function(windows) {
+        let totalWindows = windows.length;
+        let node = this.actor.get_theme_node();
+
+        // Window grid spacing
+        let columnSpacing = node.get_length('-horizontal-spacing');
+        let rowSpacing = node.get_length('-vertical-spacing');
+
+        if (!totalWindows)
+            return [];
+
+        let closeButtonHeight, captionHeight;
+        if (this._windowOverlays.length) {
+            // All of the overlays have the same chrome sizes,
+            // so just pick the first one.
+            let overlay = this._windowOverlays[0];
+            [closeButtonHeight, captionHeight] = overlay.chromeHeights();
+        } else {
+            [closeButtonHeight, captionHeight] = [0, 0];
         }
-        return slots;
+
+        rowSpacing += captionHeight;
+
+        let area = { x: this._x, y: this._y, width: this._width, height: this._height };
+        area.y += closeButtonHeight;
+        area.height -= closeButtonHeight;
+
+        let layout = this._computeLayout(windows, area, rowSpacing, columnSpacing, captionHeight);
+        let strategy = layout.strategy;
+
+        return strategy.computeWindowSlots(layout, area);
     },
 
     _onCloneSelected : function (clone, time) {